package com.wzw.springboot.principle.use.security.infrastructure.config;

import com.wzw.springboot.principle.use.cache.redis.RedisService;
import com.wzw.springboot.principle.use.security.infrastructure.filter.JwtTokenFilter;
import com.wzw.springboot.principle.use.security.infrastructure.handle.CustomizedAccessDeniedHandler;
import com.wzw.springboot.principle.use.security.infrastructure.handle.CustomizedLogoutSuccessHandler;
import com.wzw.springboot.principle.use.security.infrastructure.handle.SystemPermissionEvaluator;
import com.wzw.springboot.principle.use.security.constant.UserConstant;
import com.wzw.springboot.principle.use.security.infrastructure.point.UnAuthenticationEntryPoint;
import com.wzw.springboot.principle.use.security.infrastructure.provider.QrCodeAuthenticationProvider;
import com.wzw.springboot.principle.use.security.infrastructure.provider.SmsCodeAuthenticationProvider;
import com.wzw.springboot.principle.use.security.infrastructure.provider.ThirdPartyAuthenticationProvider;
import com.wzw.springboot.principle.use.security.manage.RbacManager;
import com.wzw.springboot.principle.use.security.manage.TokenManager;
import com.wzw.springboot.principle.use.thirdparty.loginauth.UserAuthService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.filter.CorsFilter;

/**
 * security configuration for web
 * EnableGlobalMethodSecurity
 * MethodSecurityMetadataSource 方法级别权限过滤注解元数据实现接口
 * FilterInvocationSecurityMetadataSource 配置形式的权限过滤元数据实现接口，重新实现此接口可以自定义URL级别的权限动态控制
 * securedEnabled = true @Secured 判断是否具有角色，另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“。
 * prePostEnabled = true @PreAuthorize 注解适合进入方法前的权限验证， @PreAuthorize 可以将登录用户的 roles/permissions 参数传到方法中。
 * hasRole可以不带ROLE_前缀只需要和UserDetails中的权限保持一行就行
 * prePostEnabled = true @PostAuthorize 注解使用并不多，在方法执行后再进行权限验证，适合验证带有返回值的权限.
 * other:
 * 注解@PostFilter 权限验证之后对数据进行过滤
 * 注解@PreFilter 进入控制器之前对数据进行过滤
 * SPEL 表达式实现类 SecurityExpressionRoot
 *
 * @author Wangzhiwen
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    private final String[] permitAll = new String[]{
            "/security/user/login", "/security/user/register", "/security/user/test",
            "/socket1/**", "/socket2/**"
    };

    /**
     * 使用强散列加密
     *
     * @return PasswordEncoder
     */
    @Bean
    @Primary
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 重写并暴露出认证对象
     * <p>
     * AccessDecisionManager 授权对象
     *
     * @return AuthenticationManager
     * @throws Exception e
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问（非remember-me下自动登录）
     * hasAnyAuthority     |   如果有参数，参数表示权限，则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数，参数表示角色，则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数，参数表示权限，则其权限可以访问
     * hasIpAddress        |   如果有参数，参数表示IP地址，如果用户IP和参数匹配，则可以访问
     * hasRole             |   如果有参数，参数表示角色，则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity security) throws Exception {
        ApplicationContext context = this.getApplicationContext();
        RedisService redisService = context.getBean(RedisService.class);
        RbacManager rbacManager = context.getBean(RbacManager.class);
        UserAuthService userAuthService = context.getBean(UserAuthService.class);
        CorsFilter corsFilter = context.getBean(CorsFilter.class);
        // 不使用session，禁用csrf
        security.csrf().disable()
                // 自定义认证失败的处理器
                .exceptionHandling().authenticationEntryPoint(context.getBean(UnAuthenticationEntryPoint.class))
                .and()
                // 设置session策略
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 配置允许任意访问和匿名访问的链接
                .authorizeRequests()
                // 自定义配置permissionEvaluator，如果只需要配置类中SPEL解析类只需要将类加入Bean工厂即可
                // .expressionHandler(securityExpressionHandler())
                // 配置放行地址，访问时还是会走过滤器
                .antMatchers(permitAll).permitAll()
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                // .antMatchers(HttpMethod.GET, "").permitAll()
                .antMatchers("/profile/**").anonymous()
                // 基于角色或权限进行访问控制 配合注解使用
                // 配置指定路径需要指定权限才能访问
                .antMatchers("/security/user/hasAnyAuthority").hasAnyAuthority("system:admin")
                // 配置指定路径需要指定角色才能访问 会自动给角色拼上前缀 ROLE_
                .antMatchers("/security/user/hasAnyRole").hasAnyRole(UserConstant.ROLE_ADMIN)
                // 除上述配置外的所有请求都需要认证
                .anyRequest().authenticated()
                .and()
                // 配置拒接访问的处理逻辑
                .exceptionHandling().accessDeniedHandler(context.getBean(CustomizedAccessDeniedHandler.class))
                .and()
                // 防止iframe 造成跨域
                .headers().frameOptions().disable()
                .and()
                // 配置登出地址与登出成功后的处理逻辑
                .logout().logoutUrl("/logout").logoutSuccessHandler(context.getBean(CustomizedLogoutSuccessHandler.class))
                .and()
                // 配置自定义的过滤器
                .addFilterBefore(new JwtTokenFilter(context.getBean(TokenManager.class)), UsernamePasswordAuthenticationFilter.class)
                // 跨域配置
                .addFilterBefore(corsFilter, JwtTokenFilter.class)
                .addFilterBefore(corsFilter, LogoutFilter.class);
        // 可配置自定义的认证提供商，如：扫码、短信、第三方等
        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider(rbacManager);
        smsCodeAuthenticationProvider.setRedisService(redisService);
        QrCodeAuthenticationProvider qrCodeAuthenticationProvider = new QrCodeAuthenticationProvider();
        ThirdPartyAuthenticationProvider thirdPartyAuthenticationProvider = new ThirdPartyAuthenticationProvider(rbacManager, userAuthService);
        security.authenticationProvider(smsCodeAuthenticationProvider)
                .authenticationProvider(qrCodeAuthenticationProvider)
                .authenticationProvider(thirdPartyAuthenticationProvider);

    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        // 可配置静态数据不走过滤器校验
        // web.ignoring().antMatchers(permitAll);
        ApplicationContext context = this.getApplicationContext();
    }

    /**
     * 设置自定义的用户数据查询逻辑
     *
     * @param auth AuthenticationManagerBuilder
     * @throws Exception exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        ApplicationContext context = this.getApplicationContext();
        auth.userDetailsService(context.getBean(UserDetailsService.class)).passwordEncoder(passwordEncoder());
    }

    public SecurityExpressionHandler<FilterInvocation> securityExpressionHandler() {
        DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
        handler.setPermissionEvaluator(new SystemPermissionEvaluator());
        return handler;
    }
}
